// Copyright 2013 Michel Kraemer
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package de.undercouch.citeproc;
import java.io.File;
import java.io.FileInputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import org.apache.commons.lang3.StringUtils;
import org.fusesource.jansi.Ansi;
import org.fusesource.jansi.AnsiConsole;
import de.undercouch.citeproc.csl.CSLAbbreviationList;
import de.undercouch.citeproc.csl.CSLCitation;
import de.undercouch.citeproc.csl.CSLCitationItem;
import de.undercouch.citeproc.csl.CSLItemData;
import de.undercouch.citeproc.csl.CitationIDIndexPair;
import de.undercouch.citeproc.helper.json.JsonLexer;
import de.undercouch.citeproc.helper.json.JsonParser;
import de.undercouch.citeproc.output.Bibliography;
import de.undercouch.citeproc.output.Citation;
import de.undercouch.citeproc.script.ScriptRunner;
import de.undercouch.citeproc.script.ScriptRunnerException;
import de.undercouch.citeproc.script.ScriptRunnerFactory;
import de.undercouch.citeproc.script.ScriptRunnerFactory.RunnerType;
/**
* Runs the CSL test suite (<a href="https://bitbucket.org/bdarcus/citeproc-test">https://bitbucket.org/bdarcus/citeproc-test</a>)
* @author Michel Kraemer
*/
public class TestSuiteRunner {
/**
* Main method of the test runner
* @param args the first argument can either be a compiled test file (.json)
* to run or a directory containing compiled test files
*/
public static void main(String[] args) {
TestSuiteRunner runner = new TestSuiteRunner();
runner.runTests(new File(args[0]), RunnerType.AUTO);
}
/**
* Runs tests
* @param f either a compiled test file (.json) to run or a directory
* containing compiled test files
* @param runnerType the type of the script runner that will be used
* to execute all JavaScript code
*/
public void runTests(File f, RunnerType runnerType) {
ScriptRunnerFactory.setRunnerType(runnerType);
{
ScriptRunner sr = ScriptRunnerFactory.createRunner();
System.out.println("Using script runner: " + sr.getName() +
" " + sr.getVersion());
}
//find test files
File[] testFiles;
if (f.isDirectory()) {
testFiles = f.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.endsWith(".json");
}
});
} else {
testFiles = new File[] { f };
}
AnsiConsole.systemInstall();
try {
long start = System.currentTimeMillis();
int count = testFiles.length;
int success = 0;
ExecutorService executor = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors());
//submit a job for each test file
List<Future<Boolean>> fus = new ArrayList<>();
for (File fi : testFiles) {
fus.add(executor.submit(new TestCallable(fi)));
}
//receive results
try {
for (Future<Boolean> fu : fus) {
if (fu.get()) {
++success;
}
}
} catch (Exception e) {
//should never happen
throw new RuntimeException(e);
}
executor.shutdown();
//output total time
long end = System.currentTimeMillis();
double time = (end - start) / 1000.0;
System.out.println("Successfully executed " + success + " of " + count + " tests.");
System.out.println(String.format(Locale.ENGLISH, "Total time: %.3f secs", time));
} finally {
AnsiConsole.systemUninstall();
}
}
/**
* A callable that runs a single test file
*/
private class TestCallable implements Callable<Boolean> {
private File file;
public TestCallable(File file) {
this.file = file;
}
@Override
public Boolean call() throws Exception {
Exception ex;
try {
runTest(file);
ex = null;
} catch (IllegalArgumentException | IllegalStateException | IOException e) {
ex = e;
}
synchronized (TestSuiteRunner.this) {
//output name
String name = file.getName().substring(0, file.getName().length() - 5);
System.out.print(name);
for (int i = 0; i < (79 - name.length() - 9); ++i) {
System.out.print(" ");
}
//output result
if (ex == null) {
System.out.println("[" + Ansi.ansi().fg(Ansi.Color.GREEN)
.a("SUCCESS").reset() + "]");
return Boolean.TRUE;
} else {
System.out.println("[" + Ansi.ansi().fg(Ansi.Color.RED)
.a("FAILURE").reset() + "]");
System.err.println(ex.getMessage());
return Boolean.FALSE;
}
}
}
}
/**
* Runs a single test file
* @param f the test file to run
* @throws IOException if the file could not be loaded
*/
@SuppressWarnings("unchecked")
private static void runTest(File f) throws IOException {
//load file
Map<String, Object> conf = readFile(f);
//get configuration
String mode = (String)conf.get("mode");
String result = (String)conf.get("result");
String style = (String)conf.get("csl");
Collection<Map<String, Object>> input =
(Collection<Map<String, Object>>)conf.get("input");
Collection<List<Object>> rawCitations =
(Collection<List<Object>>)conf.get("citations");
Collection<List<Map<String, Object>>> rawCitationItems =
(Collection<List<Map<String, Object>>>)conf.get("citation_items");
Map<String, Map<String, Object>> abbreviations =
(Map<String, Map<String, Object>>)conf.get("abbreviations");
Collection<List<String>> bibentries =
(Collection<List<String>>)conf.get("bibentries");
Map<String, Collection<Map<String, Object>>> rawBibsection =
(Map<String, Collection<Map<String, Object>>>)conf.get("bibsection");
//parse mode
String[] modes = mode.split("-");
mode = modes[0];
Set<String> submodes = new HashSet<>();
for (int i = 1; i < modes.length; ++i) {
submodes.add(modes[i]);
}
//convert item data
int i = 0;
CSLItemData[] items = new CSLItemData[input.size()];
for (Map<String, Object> m : input) {
items[i++] = CSLItemData.fromJson(m);
}
//convert citations
List<List<Object>> citations = null;
if (rawCitations != null) {
citations = new ArrayList<>();
for (List<Object> m : rawCitations) {
List<Object> cits = new ArrayList<>();
cits.add(CSLCitation.fromJson((Map<String, Object>)m.get(0)));
Collection<List<Object>> coll1 = (Collection<List<Object>>)m.get(1);
Collection<List<Object>> coll2 = (Collection<List<Object>>)m.get(2);
List<CitationIDIndexPair> citsPre = new ArrayList<>();
List<CitationIDIndexPair> citsPost = new ArrayList<>();
for (List<Object> c1m : coll1) {
citsPre.add(CitationIDIndexPair.fromJson(c1m));
}
for (List<Object> c2m : coll2) {
citsPost.add(CitationIDIndexPair.fromJson(c2m));
}
cits.add(citsPre);
cits.add(citsPost);
citations.add(cits);
}
}
//convert citation items
List<List<CSLCitationItem>> citationItems = null;
if (rawCitationItems != null) {
citationItems = new ArrayList<>();
for (List<Map<String, Object>> l : rawCitationItems) {
List<CSLCitationItem> cits = new ArrayList<>();
for (Map<String, Object> m : l) {
cits.add(CSLCitationItem.fromJson(m));
}
citationItems.add(cits);
}
}
//convert abbreviations
DefaultAbbreviationProvider abbreviationProvider = new DefaultAbbreviationProvider();
if (abbreviations != null) {
for (Map.Entry<String, Map<String, Object>> e : abbreviations.entrySet()) {
CSLAbbreviationList al = CSLAbbreviationList.fromJson(e.getValue());
abbreviationProvider.add(e.getKey(), al);
}
}
//convert the 'bibsection' configuration
SelectionMode bibSectionMode = null;
CSLItemData[] bibSection = null;
CSLItemData[] bibSectionQuash = null;
if (rawBibsection != null) {
for (Map.Entry<String, Collection<Map<String, Object>>> e : rawBibsection.entrySet()) {
CSLItemData[] r = convertBibSection(e.getValue());
if (e.getKey().equals("quash")) {
bibSectionQuash = r;
} else {
bibSection = r;
bibSectionMode = SelectionMode.fromString(e.getKey());
}
}
}
//create CSL processor
ListItemDataProvider itemDataProvider = new ListItemDataProvider(items);
TestSuiteCSL citeproc = new TestSuiteCSL(itemDataProvider, abbreviationProvider, style);
//set output format
if (submodes.contains("rtf")) {
citeproc.setOutputFormat("rtf");
}
//set development options
Map<String, Object> options = (Map<String, Object>)conf.get("options");
if (options != null) {
for (Map.Entry<String, Object> e : options.entrySet()) {
if (e.getKey().equals("variableWrapper")) {
continue;
}
citeproc.setDevelopmentExtension(e.getKey(), e.getValue());
}
}
//register citation items
boolean nosort = submodes.contains("nosort");
if (bibentries != null) {
for (List<String> be : bibentries) {
citeproc.registerCitationItems(be.toArray(new String[be.size()]), nosort);
}
} else if (citations == null) {
citeproc.registerCitationItems(itemDataProvider.getIds(), nosort);
}
//set default citation items
if (citations == null && citationItems == null) {
citationItems = new ArrayList<>();
citationItems.add(citeproc.getRegistryReflist());
}
//make citations
String citationResult = null;
if (citationItems != null) {
citationResult = "";
for (List<CSLCitationItem> cits : citationItems) {
if (citationResult.length() > 0) {
citationResult += "\n";
}
citationResult += citeproc.makeCitationCluster(
cits.toArray(new CSLCitationItem[cits.size()]));
}
} else if (citations != null) {
List<List<Object>> slice = citations.subList(0, citations.size() - 1);
for (List<Object> cit : slice) {
citeproc.makeCitation((CSLCitation)cit.get(0),
(List<CitationIDIndexPair>)cit.get(1),
(List<CitationIDIndexPair>)cit.get(2));
}
List<Object> citation = citations.get(citations.size() - 1);
List<Citation> r = citeproc.makeCitation((CSLCitation)citation.get(0),
(List<CitationIDIndexPair>)citation.get(1),
(List<CitationIDIndexPair>)citation.get(2));
Map<Integer, Integer> indexMap = new HashMap<>();
int pos = 0;
for (Citation c : r) {
indexMap.put(c.getIndex(), pos);
++pos;
}
List<String> resultCitations = new ArrayList<>();
for (int cpos = 0; cpos < citeproc.getCitationsByIndex().size(); ++cpos) {
if (indexMap.containsKey(cpos)) {
resultCitations.add(">>[" + cpos + "] " + r.get(indexMap.get(cpos)).getText());
} else {
resultCitations.add("..[" + cpos + "] " + citeproc.callProcessCitationCluster(cpos));
}
}
citationResult = StringUtils.join(resultCitations, "\n");
}
//make bibliography
if (mode.equals("bibliography") && !submodes.contains("header")) {
if (bibSection != null || bibSectionQuash != null) {
citationResult = citeproc.makeBibliography(bibSectionMode,
bibSection, bibSectionQuash).makeString();
} else {
citationResult = citeproc.makeBibliography().makeString();
}
} else if (submodes.contains("header")) {
Bibliography p = citeproc.makeBibliography();
citationResult = "";
citationResult += "bibend: " + p.getBibEnd() + "\n";
citationResult += "bibliography_errors: \n"; //TODO not implemented yet
citationResult += "bibstart: " + p.getBibStart() + "\n";
citationResult += "done: " + p.getDone() + "\n";
citationResult += "entry_ids: " + StringUtils.join(p.getEntryIds(), ",") + "\n";
citationResult += "entryspacing: " + p.getEntrySpacing() + "\n";
citationResult += "linespacing: " + p.getLineSpacing() + "\n";
citationResult += "maxoffset: " + p.getMaxOffset() + "\n";
citationResult += "second-field-align: " + p.getSecondFieldAlign();
}
//compare result
if (!result.equals(citationResult)) {
throw new IllegalStateException("expected: <" + result +
"> but was <" + citationResult + ">");
}
}
/**
* Reads a compiled test file (.json)
* @param f the test file
* @return the configuration read from the file
* @throws IOException if the file could not be read
*/
private static Map<String, Object> readFile(File f) throws IOException {
try (FileInputStream fis = new FileInputStream(f)) {
JsonLexer jsonLexer = new JsonLexer(new InputStreamReader(fis, "UTF-8"));
JsonParser jsonParser = new JsonParser(jsonLexer);
Map<String, Object> m = jsonParser.parseObject();
//remove items whose value is 'false'
Iterator<Map.Entry<String, Object>> i = m.entrySet().iterator();
while (i.hasNext()) {
Map.Entry<String, Object> e = i.next();
if (e.getValue() instanceof Boolean && !((Boolean)e.getValue()).booleanValue()) {
i.remove();
}
}
return m;
}
}
/**
* Convert a 'bibsection' configuration to a list of item data objects
* @param bs the configuration
* @return the item data objects
*/
private static CSLItemData[] convertBibSection(Collection<Map<String, Object>> bs) {
CSLItemData[] r = new CSLItemData[bs.size()];
int i = 0;
for (Map<String, Object> s : bs) {
String f = (String)s.get("field");
Object v = s.get("value");
if (f.equals("issued") && ((String)v).isEmpty()) {
v = new HashMap<String, Object>();
} else if (f.equals("categories")) {
v = Arrays.asList(v);
}
Map<String, Object> m = new HashMap<>();
m.put(f, v);
r[i++] = CSLItemData.fromJson(m);
}
return r;
}
/**
* A special citation processor that allows access to the internal API
*/
private static class TestSuiteCSL extends CSL {
private static class TestSuiteLocaleProvider extends DefaultLocaleProvider {
@Override
public String retrieveLocale(String lang) {
try {
return super.retrieveLocale(lang);
} catch (IllegalArgumentException e) {
// fall back to empty locale definition for invalid lang tags
return "[]";
}
}
}
public TestSuiteCSL(ItemDataProvider itemDataProvider,
AbbreviationProvider abbreviationProvider, String style)
throws IOException {
super(itemDataProvider, new TestSuiteLocaleProvider(),
abbreviationProvider, style, "en-US", false);
ScriptRunner sr = getScriptRunner();
try {
sr.eval(new StringReader(
"function __getCitationByIndex(engine) { "
+ "return engine.registry.citationreg.citationByIndex; }"
+ "function __callProcessCitationCluster(engine, cpos) { "
+ "return engine.process_CitationCluster("
+ "engine.registry.citationreg.citationByIndex[cpos].sortedItems); }"
+ "function __getRefList(engine) {"
+ "return engine.registry.reflist; }"
+ "function __setDevelopmentExtension(engine, key, value) {"
+ "engine.opt.development_extensions[key] = value; }"
));
} catch (ScriptRunnerException e) {
throw new IOException("Could not evaluate inline scripts", e);
}
}
public List<CSLCitation> getCitationsByIndex() {
List<?> r;
try {
r = getScriptRunner().callMethod("__getCitationByIndex",
List.class, getEngine());
} catch (ScriptRunnerException e) {
throw new IllegalArgumentException("Could not get registered citations", e);
}
List<CSLCitation> result = new ArrayList<>();
for (Object o : r) {
@SuppressWarnings("unchecked")
Map<String, Object> m = (Map<String, Object>)o;
result.add(CSLCitation.fromJson(m));
}
return result;
}
public String callProcessCitationCluster(int cpos) {
try {
return getScriptRunner().callMethod("__callProcessCitationCluster",
String.class, getEngine(), cpos);
} catch (ScriptRunnerException e) {
throw new IllegalArgumentException("Could not get registered citations", e);
}
}
public List<CSLCitationItem> getRegistryReflist() {
List<?> r;
try {
r = getScriptRunner().callMethod("__getRefList", List.class, getEngine());
} catch (ScriptRunnerException e) {
throw new IllegalArgumentException("Could not get registered citation items", e);
}
List<CSLCitationItem> result = new ArrayList<>();
for (Object o : r) {
@SuppressWarnings("unchecked")
Map<String, Object> m = (Map<String, Object>)o;
result.add(CSLCitationItem.fromJson(m));
}
return result;
}
public String makeCitationCluster(CSLCitationItem... citation) {
try {
return getScriptRunner().callMethod(getEngine(),
"makeCitationCluster", String.class, (Object)citation);
} catch (ScriptRunnerException e) {
throw new IllegalArgumentException("Could not make citation custer", e);
}
}
public void setDevelopmentExtension(String key, Object value) {
try {
getScriptRunner().callMethod("__setDevelopmentExtension", getEngine(), key, value);
} catch (ScriptRunnerException e) {
throw new IllegalArgumentException("Could not set development extension", e);
}
}
}
}